package com.fuhailiu.opengl.widget;

import java.nio.ByteBuffer;
import java.nio.FloatBuffer;

import javax.microedition.khronos.opengles.GL10;

import android.content.Context;
import android.graphics.Color;
import android.opengl.GLES30;

import com.fuhailiu.opengl.utils.BenchmarkUtil;
import com.fuhailiu.opengl.utils.GlUtil;
import com.fuhailiu.opengl.utils.LogUtil;
import com.fuhailiu.opengl.utils.MatrixUtil;
import com.fuhailiu.opengl.utils.ShaderUtil;
import com.fuhailiu.opengl.view.GLView;

/**
 * testing Pixel Buffer Object for packing (read-back) pixel data from frame
 * buffer to a PBO
 */
public class GLPBOPack extends GLView {

	private final String TAG = getClass().getSimpleName();

	private int mSurfaceWidth, mSurfaceHeight;
	private float mRatio;
	private final int DATA_SIZE;

	public GLPBOPack(Context context, int width, int height) {
		super(context);
		mSurfaceWidth = width;
		mSurfaceHeight = height;
		mRatio = 1.0f * mSurfaceWidth / 2 / mSurfaceHeight;
		DATA_SIZE = mSurfaceWidth / 2 * mSurfaceHeight * GlUtil.BYTES_PER_PIXEL;
		initRectData();
		initRectShader();
		initTextureData();
		initTextureShader();
		createTexture();
		initPBO();
		initData();
	}

	public final float x = 0.2f;
	public final float y = 0.2f;

	// 顶点坐标数据
	private float rectVertices[] = new float[] { 
			-x, -y, 0f, // 0 bottom left
			x, -y, 0f, // 1 bottom right
			-x, y, 0f, // 2 top left
			x, y, 0f, // 3 top right
	};

	// 顶点颜色数据
	private float rectColors[] = new float[] { 
			1.0f, 0f, 0f, 1.0f, // red
			0f, 1.0f, 0f, 1.0f, // green
			0f, 0f, 1.0f, 1.0f, // blue
			1.0f, 1.0f, 0f, 1.0f,// yellow
	};

	// 顶点坐标数据缓冲
	private FloatBuffer mBufferRectVertex;
	// 顶点颜色数据缓冲
	private FloatBuffer mBufferRectColor;

	private void initRectData() {
		mBufferRectVertex = GlUtil.createFloatBuffer(rectVertices);
		mBufferRectColor = GlUtil.createFloatBuffer(rectColors);
	}

	private static final String RECT_VERTEX_SHADER = 
			"uniform mat4 uMVPMatrix; //总变换矩阵\n" + 
			"attribute vec3 aPosition; //顶点位置\n" + 
			"attribute vec4 aColor; //顶点颜色\n" + 
			"varying vec4 vColor; //用于传递给片元着色器的变量\n" + 
			"void main()\n" + 
			"{\n" + 
			"	gl_Position = uMVPMatrix * vec4(aPosition,1); //根据总变换矩阵计算此次绘制此顶点位置\n" + 
			"	vColor = aColor; //将接收的颜色传递给片元着色器 \n" + 
			"}\n";

	private static final String RECT_FRAGMENT_SHADER = 
			"precision mediump float;\n" + 
			"varying vec4 vColor; //接收从顶点着色器过来的参数\n" + 
			"void main()\n" + 
			"{\n" + 
			"	gl_FragColor = vColor; //给此片元颜色值\n" + 
			"}\n";

	// 自定义渲染管线程序id
	private int handleRectProgram;
	// 顶点位置属性引用id
	private int handleRectPosition;
	// 顶点颜色属性引用id
	private int handleRectColor;
	// 总变换矩阵引用id
	private int handleRectMVPMatrix;

	private void initRectShader() {
		LogUtil.LogD(TAG, "initRectShader   <---");

		// 基于顶点着色器与片元着色器创建程序
		handleRectProgram = ShaderUtil.createProgram(RECT_VERTEX_SHADER, RECT_FRAGMENT_SHADER);
		// 获取程序中顶点位置属性引用id
		handleRectPosition = GLES30.glGetAttribLocation(handleRectProgram, "aPosition");
		// 获取顶点颜色引用id
		handleRectColor = GLES30.glGetAttribLocation(handleRectProgram, "aColor");
		// 获取程序中总变换矩阵引用id
		handleRectMVPMatrix = GLES30.glGetUniformLocation(handleRectProgram, "uMVPMatrix");

		LogUtil.LogD(TAG, "initRectShader   --->");
	}

	// 纹理坐标数据
	private float textureCoords[] = new float[] { 
			0.0f, 0.0f, // 0 bottom left
			1.0f, 0.0f, // 1 bottom right
			0.0f, 1.0f, // 2 top left
			1.0f, 1.0f, // 3 top right
	};

	// 顶点坐标数据缓冲
	private FloatBuffer mBufferTexVertex;
	// 纹理坐标数据缓冲
	private FloatBuffer mBufferTexCoord;
	private ByteBuffer mBufferPixel;

	private void initTextureData() {
		LogUtil.LogD(TAG, "initTextureData   <---");

		float x = mRatio;
		float y = 1.0f;
		// 顶点坐标数据
		float vertices[] = new float[] { 
				-x, -y, 0f, // 0 bottom left
				x, -y, 0f, // 1 bottom right
				-x, y, 0f, // 2 top left
				x, y, 0f, // 3 top right
		};
		mBufferTexVertex = GlUtil.createFloatBuffer(vertices);
		mBufferTexCoord = GlUtil.createFloatBuffer(textureCoords);

		mBufferPixel = ByteBuffer.allocate(DATA_SIZE);

		LogUtil.LogD(TAG, "initTextureData   --->");
	}

	private final String TEX_VERTEX_SHADER = 
			"uniform mat4 uMVPMatrix;\n" + 
			"attribute vec3 aPosition;\n" + 
			"attribute vec2 aTextureCoord;\n" + 
			"varying vec2 vTextureCoord;\n" + 
			"void main() {\n" + 
			"    gl_Position = uMVPMatrix * vec4(aPosition, 1);\n" + 
			"    vTextureCoord = aTextureCoord;\n" + 
			"}\n";

	private final String TEX_FRAGMENT_SHADER = 
				"precision mediump float;\n" + 
				"varying vec2 vTextureCoord;\n" + 
				"uniform sampler2D sTexture;\n" + 
				"void main() {\n" + 
				"    gl_FragColor = texture2D(sTexture, vTextureCoord);\n" + 
				"}\n";

	// 自定义渲染管线程序id
	private int handleTexProgram;
	// 顶点位置属性引用id
	private int handleTexPosition;
	// 纹理坐标属性引用id
	private int handleTexCoord;
	// 总变换矩阵引用id
	private int handleTexMVPMatrix;

	private void initTextureShader() {
		LogUtil.LogD(TAG, "initTextureShader   <---");

		// 基于顶点着色器与片元着色器创建程序
		handleTexProgram = ShaderUtil.createProgram(TEX_VERTEX_SHADER, TEX_FRAGMENT_SHADER);
		// 获取程序中顶点位置属性引用id
		handleTexPosition = GLES30.glGetAttribLocation(handleTexProgram, "aPosition");
		// 获取程序中纹理坐标引用id
		handleTexCoord = GLES30.glGetAttribLocation(handleTexProgram, "aTextureCoord");
		// 获取程序中总变换矩阵引用id
		handleTexMVPMatrix = GLES30.glGetUniformLocation(handleTexProgram, "uMVPMatrix");

		LogUtil.LogD(TAG, "initTextureShader   --->");
	}

	private int mTextureId;

	private void createTexture() {
		LogUtil.LogD(TAG, "createTexture   <---");

		int[] textureIds = new int[1];
		GLES30.glGenTextures(1, textureIds, 0);

		int textureId = textureIds[0];
		GLES30.glBindTexture(GLES30.GL_TEXTURE_2D, textureId);
		GLES30.glTexParameterf(GLES30.GL_TEXTURE_2D, GLES30.GL_TEXTURE_MIN_FILTER, GLES30.GL_NEAREST);
		GLES30.glTexParameterf(GLES30.GL_TEXTURE_2D, GLES30.GL_TEXTURE_MAG_FILTER, GLES30.GL_LINEAR);
		GLES30.glTexParameterf(GLES30.GL_TEXTURE_2D, GLES30.GL_TEXTURE_WRAP_S, GLES30.GL_CLAMP_TO_EDGE);
		GLES30.glTexParameterf(GLES30.GL_TEXTURE_2D, GLES30.GL_TEXTURE_WRAP_T, GLES30.GL_CLAMP_TO_EDGE);
		GLES30.glTexImage2D(GLES30.GL_TEXTURE_2D, 0, GLES30.GL_RGBA, mSurfaceWidth / 2, mSurfaceHeight, 0, GLES30.GL_RGBA, GLES30.GL_UNSIGNED_BYTE, null);

		mTextureId = textureId;

		LogUtil.LogD(TAG, "createTexture   --->");
	}

	private final int bufferCount = 2;
	private int[] pboIds;

	private void initPBO() {
		LogUtil.LogD(TAG, "initPBO   <---");

		// create 2 pixel buffer objects, glBufferData with NULL reserves only
		// memory space.
		pboIds = new int[bufferCount];
		GLES30.glGenBuffers(bufferCount, pboIds, 0);
		GlUtil.checkGlError("glGenBuffers");

		for (int i = 0; i < bufferCount; i++) {
			GLES30.glBindBuffer(GLES30.GL_PIXEL_PACK_BUFFER, pboIds[i]);
			GLES30.glBufferData(GLES30.GL_PIXEL_PACK_BUFFER, DATA_SIZE, null, GLES30.GL_STREAM_READ);
		}

		// 解除绑定
		GLES30.glBindBuffer(GLES30.GL_PIXEL_PACK_BUFFER, 0);

		LogUtil.LogD(TAG, "initPBO   --->");
	}

	@Override
	public void release() {
		LogUtil.LogD(TAG, "release   <---");

		super.release();

		GLES30.glDeleteTextures(1, new int[] { mTextureId }, 0);
		GlUtil.checkGlError("glDeleteTextures");

		GLES30.glDeleteBuffers(bufferCount, pboIds, 0);
		GlUtil.checkGlError("glDeleteBuffers");

		LogUtil.LogD(TAG, "release   --->");
	}

	public static final int PBO_OFF = 0;
	public static final int PBO_ON = 1;
	private int mMode = PBO_OFF;

	public void setMode(int mode) {
		LogUtil.LogD(TAG, "setMode   <---");
		LogUtil.LogD(TAG, "setMode   mode = " + mode);
		mMode = mode;
		LogUtil.LogD(TAG, "setMode   --->");
	}

	private int pboIndex = 0;
	private final boolean VERBOSE = false;

	public void draw(GL10 gl) {
		// draw left rect
		drawLeft();
		GlUtil.checkGlError("drawLeft");

		if (mMode == PBO_OFF) {
			if (VERBOSE) {
				BenchmarkUtil.start("ReadData");
			}
			// copy pixels from frame buffer
			GLES30.glReadPixels(0, 0, mSurfaceWidth / 2, mSurfaceHeight, GLES30.GL_RGBA, GLES30.GL_UNSIGNED_BYTE, mBufferPixel);
			if (VERBOSE) {
				BenchmarkUtil.stop("ReadData");
			}

			if (VERBOSE) {
				BenchmarkUtil.start("UpdateData");
			}
			// update pixel
			updatePixels(mBufferPixel);
			if (VERBOSE) {
				BenchmarkUtil.stop("UpdateData");
			}
		} else {
			// "pboIndex" is used to read pixels from a frame buffer to a PBO
			// "nextPboIndex" is used to process pixels in the other PBO
			pboIndex = (pboIndex + 1) % 2;
			int nextPboIndex = (pboIndex + 1) % 2;

			if (VERBOSE) {
				BenchmarkUtil.start("ReadData");
			}
			// copy pixels from frame buffer to PBO
			// OpenGL should perform asynch DMA transfer, so glReadPixels() will
			// return immediately.
			GLES30.glBindBuffer(GLES30.GL_PIXEL_PACK_BUFFER, pboIds[pboIndex]);
			// TODO: OpenGL ES don't support copy pixels from frame buffer to PBO currently
//			GLES30.glReadPixels(0, 0, mSurfaceWidth / 2, mSurfaceHeight, GLES30.GL_RGBA, GLES30.GL_UNSIGNED_BYTE, null);
			if (VERBOSE) {
				BenchmarkUtil.stop("ReadData");
			}

			if (VERBOSE) {
				BenchmarkUtil.start("UpdateData");
			}
			// map the PBO that contain frame buffer pixels before processing it
			GLES30.glBindBuffer(GLES30.GL_PIXEL_PACK_BUFFER, pboIds[nextPboIndex]);
			ByteBuffer buffer = (ByteBuffer) GLES30.glMapBufferRange(GLES30.GL_PIXEL_PACK_BUFFER, 0, DATA_SIZE, GLES30.GL_MAP_WRITE_BIT | GLES30.GL_MAP_INVALIDATE_BUFFER_BIT);
			// update data directly on the mapped buffer
			updatePixels(buffer);
			// release pointer to mapping buffer
			GLES30.glUnmapBuffer(GLES30.GL_PIXEL_PACK_BUFFER);
			if (VERBOSE) {
				BenchmarkUtil.stop("UpdateData");
			}

			// release PBOs with ID 0 after use.
			GLES30.glBindBuffer(GLES30.GL_PIXEL_PACK_BUFFER, 0);
		}

		// draw right rect
		drawRight();
		GlUtil.checkGlError("drawRight");
	}

	public volatile float mAngleX = 0;
	public volatile float mAngleY = 0;

	private void drawLeft() {
		GLES30.glViewport(0, 0, mSurfaceWidth / 2, mSurfaceHeight);
		MatrixUtil.setProjectOrtho(-mRatio, mRatio, -1.0f, 1.0f, 1.0f, 3.0f);

		// 保存当前矩阵
		MatrixUtil.pushMatrix();
		MatrixUtil.rotate(mAngleX, 1, 0, 0);
		MatrixUtil.rotate(mAngleY, 0, 1, 0);

		// 使用shader程序
		GLES30.glUseProgram(handleRectProgram);
		// 指定顶点位置数据
		GLES30.glVertexAttribPointer(handleRectPosition, 3, GLES30.GL_FLOAT, false, 3 * GlUtil.SIZE_OF_FLOAT, mBufferRectVertex);
		// 指定顶点颜色数据
		GLES30.glVertexAttribPointer(handleRectColor, 4, GLES30.GL_FLOAT, false, 4 * GlUtil.SIZE_OF_FLOAT, mBufferRectColor);
		// 指定变换矩阵
		GLES30.glUniformMatrix4fv(handleRectMVPMatrix, 1, false, MatrixUtil.getMVPMatrix(), 0);
		// 启用顶点位置数组
		GLES30.glEnableVertexAttribArray(handleRectPosition);
		// 启用顶点颜色数组
		GLES30.glEnableVertexAttribArray(handleRectColor);
		// 绘制三角形
		GLES30.glDrawArrays(GLES30.GL_TRIANGLE_STRIP, 0, 4);
		// 禁用顶点颜色数组
		GLES30.glDisableVertexAttribArray(handleRectColor);
		// 禁用顶点颜色数组
		GLES30.glDisableVertexAttribArray(handleRectPosition);

		// 恢复矩阵
		MatrixUtil.popMatrix();
	}

	private void drawRight() {
		GLES30.glViewport(mSurfaceWidth / 2, 0, mSurfaceWidth / 2, mSurfaceHeight);
		MatrixUtil.setProjectOrtho(-mRatio, mRatio, -1.0f, 1.0f, 1.0f, 3.0f);

		// 使用shader程序
		GLES30.glUseProgram(handleTexProgram);
		GLES30.glActiveTexture(GLES30.GL_TEXTURE0);
		GLES30.glBindTexture(GLES30.GL_TEXTURE_2D, mTextureId);
		GLES30.glTexImage2D(GLES30.GL_TEXTURE_2D, 0, GLES30.GL_RGBA, mSurfaceWidth / 2, mSurfaceHeight, 0, GLES30.GL_RGBA, GLES30.GL_UNSIGNED_BYTE, mBufferPixel);

		// 指定顶点位置数据
		GLES30.glVertexAttribPointer(handleTexPosition, 3, GLES30.GL_FLOAT, false, 3 * GlUtil.SIZE_OF_FLOAT, mBufferTexVertex);
		// 指定纹理坐标数据
		GLES30.glVertexAttribPointer(handleTexCoord, 2, GLES30.GL_FLOAT, false, 2 * GlUtil.SIZE_OF_FLOAT, mBufferTexCoord);
		// 指定变换矩阵
		GLES30.glUniformMatrix4fv(handleTexMVPMatrix, 1, false, MatrixUtil.getMVPMatrix(), 0);
		// 启用顶点位置数组
		GLES30.glEnableVertexAttribArray(handleTexPosition);
		// 启用纹理坐标数组
		GLES30.glEnableVertexAttribArray(handleTexCoord);
		// 绘制三角形
		GLES30.glDrawArrays(GLES30.GL_TRIANGLE_STRIP, 0, 4);
		// 禁用顶点位置数组
		GLES30.glDisableVertexAttribArray(handleTexCoord);
		// 禁用顶点颜色数组
		GLES30.glDisableVertexAttribArray(handleTexPosition);
	}

	private final int[] COLORS = new int[] { 
			Color.RED, 
			Color.GREEN, 
			Color.BLUE, 
			Color.YELLOW, 
			Color.CYAN, 
			Color.MAGENTA, };

	private int mColorIndex = 0;

	private int getNextColor() {
		int color = COLORS[mColorIndex];
		mColorIndex++;
		mColorIndex = mColorIndex % COLORS.length;
		return color;
	}

	private final int WIDTH = 100;
	private final int HEIGHT = 100;

	private int LEFT;
	private int TOP;

	private void initData() {
		LEFT = (mSurfaceWidth / 2 - WIDTH) / 2;
		TOP = (mSurfaceHeight - HEIGHT) / 2;
	}

	private void updatePixels(ByteBuffer pixels) {
		if (null == pixels)
			return;
		updatePixelsArea(pixels, LEFT, TOP, WIDTH, HEIGHT, getNextColor());
	}

	private void updatePixelsArea(ByteBuffer pixels, int x, int y, int width, int height, int color) {
		int bytesPerRow = mSurfaceWidth / 2 * GlUtil.BYTES_PER_PIXEL;

		byte red = intToByte(Color.red(color));
		byte green = intToByte(Color.green(color));
		byte blue = intToByte(Color.blue(color));
		byte alpha = intToByte(Color.alpha(color));

		for (int row = y; row < y + height; row++) {
			for (int col = x; col < x + width; col++) {
				int index = row * bytesPerRow + col * GlUtil.BYTES_PER_PIXEL;
				pixels.put(index, red);
				pixels.put(index + 1, green);
				pixels.put(index + 2, blue);
				pixels.put(index + 3, alpha);
			}
		}
	}

	private byte intToByte(int value) {
		return (byte) value;
	}

}
